package com.xing.game.bean;

import com.xing.game.bean.api.Bean;
import com.xing.game.bean.api.BeanFactory;
import com.xing.game.bean.api.BeanProcessorChain;
import com.xing.game.bean.api.ProcessingBean;
import com.xing.game.bean.exception.BeanCheckException;
import com.xing.game.bean.exception.BeanInstantiationException;
import com.xing.game.bean.exception.BeanNotFoundException;
import com.xing.game.bean.exception.BeanProcessException;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;

public class DefaultBeanFactory implements BeanFactory{

    private final ConcurrentHashMap<Class<?>, Object> beanTypes = new ConcurrentHashMap<>();

    private List<DefaultBeanProcessorChain> beanPostProcessors = new CopyOnWriteArrayList<DefaultBeanProcessorChain>();

    private AutoInjectProcessorChain autoInjectProcessor = new AutoInjectProcessorChain(this);

    public DefaultBeanFactory() {
        beanPostProcessors.add(new DefaultBeanProcessorChain());
    }

    @Override
    public <T> T getBean(Class<T> beanClass) throws ExecutionException {
        final Object bean = beanTypes.get(beanClass);
        if(bean == null){
            throw new ExecutionException(new BeanNotFoundException(beanClass.getName()));
        }
        return (T)bean;
    }


    private Bean checkBean(Class<?> clazz) throws BeanCheckException{
        Bean bean = clazz.getAnnotation(Bean.class);
        if(bean == null){
            throw new BeanCheckException(clazz, "This class does not annotate @Bean");
        }
        return bean;
    }

    private void checkBeanType(Class<?> beanType, Class<?> objClass) throws BeanCheckException{
        if(!beanType.isAssignableFrom(objClass)){
            throw new BeanCheckException(objClass, "this object is not an instance of " + beanType);
        }
    }

    private void checkAndLinkBeanTypes(Class<?> beanClass, Class<?>[] beanTypesRef) throws BeanCheckException{
        for(Class<?> beanType : beanTypesRef){
            if(beanType != Object.class){
                checkBeanType(beanType, beanClass);
            }
        }
    }

    private <T> T newBeanInstance(final Class<T> clazz) throws BeanInstantiationException, BeanProcessException{
        T obj;
        try {
            obj = clazz.newInstance();
        } catch (InstantiationException e) {
            throw new BeanInstantiationException(clazz, e.getMessage(), e);
        } catch (IllegalAccessException e) {
            throw new BeanInstantiationException(clazz, e.getMessage(), e);
        }

        try {
            //自动注入
            obj = cloneProcessorChain(autoInjectProcessor).doProcess(obj);

            //后处理
            obj = doBeanPostProcess(obj);

            return obj;
        }catch(Exception e) {
            throw new BeanInstantiationException(clazz, e.getMessage(), e);
        }
    }

    private BeanProcessorChain cloneProcessorChain(BeanProcessorChain chain) throws BeanProcessException{
        try {
            return (BeanProcessorChain)chain.clone();
        } catch (CloneNotSupportedException e) {
            throw new BeanProcessException("can not clone ProcessorChain", e);
        }
    }

    private <T> T doBeanPostProcess(T obj) throws BeanProcessException{

        //Bean后处理
        for(DefaultBeanProcessorChain chain: beanPostProcessors){
            if(!chain.isMatch(obj.getClass())){
                continue;
            }
            obj = cloneProcessorChain(chain).doProcess(obj);
        }

        //后处理完成通知
        if(obj instanceof ProcessingBean){
            ((ProcessingBean) obj).afterProcessComplete();
        }

        return obj;
    }



    @Override
    public <T> T registBean(Class<T> beanClass) throws BeanInstantiationException, BeanCheckException, BeanProcessException {
        Bean bean = checkBean(beanClass);

        Class<?>[] beanTypesRef = bean.type();
        checkAndLinkBeanTypes(beanClass, beanTypesRef);

        T obj = newBeanInstance(beanClass);

        beanTypes.put(beanClass, obj);

        return obj;
    }


}
